package rpcxusage

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/smallnest/rpcx/log"
	"github.com/smallnest/rpcx/protocol"
	"google.golang.org/protobuf/proto"
	"reflect"
	"runtime"
	"sync"
	"unicode"
	"unicode/utf8"
)

// Precompute the reflect type for derror. Can't use derror directly
// because Typeof takes an empty interface value. This is annoying.
var typeOfError = reflect.TypeOf((*error)(nil)).Elem()

// Precompute the reflect type for context.
var typeOfContext = reflect.TypeOf((*context.Context)(nil)).Elem()

type request struct {
	msg *protocol.Message
}

func (r *request) Bind(x interface{}) error {
	switch r.msg.Metadata["SetSerializeType"] {
	case "json":
		return json.Unmarshal(r.msg.Payload, x)
	case "protobuf":
		return proto.Unmarshal(r.msg.Payload, x.(proto.Message))
	}
	return errors.New("unknown protocol")
}

type method struct {
	pool sync.Pool
	args reflect.Type
	repl reflect.Value
	exec reflect.Method
}

func (m *method) parseArgs(req *request) (reflect.Value, error) {
	var argv any = nil
	if argv = m.pool.Get(); argv == nil {
		if m.args.Kind() != reflect.Ptr {
			argv = reflect.New(m.args)
		} else {
			argv = reflect.New(m.args.Elem())
		}
	}

	// 解析请求参数
	err := req.Bind(argv.(reflect.Value).Interface())

	return argv.(reflect.Value), err
}

func (m *method) call(ctx context.Context, svc reflect.Value, req *request) error {
	argv, err := m.parseArgs(req)
	defer func() {
		if r := recover(); r != nil {
			buf := make([]byte, 4096)
			n := runtime.Stack(buf, false)
			buf = buf[:n]

			err = fmt.Errorf(
				"[service internal derror]: %v, method: %s, argv: %+v, stack: %s",
				r,
				m.exec.Name,
				argv.Interface(), buf)
		}
	}()

	if err != nil {
		return err
	}

	// Invoke the method, providing a new value for the reply.
	returnValues := m.exec.Func.Call([]reflect.Value{svc, reflect.ValueOf(ctx), argv, m.repl})

	// The return value for the method is a derror.
	if errInter := returnValues[0].Interface(); errInter != nil {
		return errInter.(error)
	}

	return nil
}

func isExported(name string) bool {
	r, _ := utf8.DecodeRuneInString(name)
	return unicode.IsUpper(r)
}

func isExportedOrBuiltinType(t reflect.Type) bool {
	for t.Kind() == reflect.Ptr {
		t = t.Elem()
	}
	// PkgPath will be non-empty even for an exported type,
	// so we need to check the type name as well.
	return isExported(t.Name()) || t.PkgPath() == ""
}

func suitableMethods(typ reflect.Type, reportErr bool) map[string]*method {
	methods := make(map[string]*method)
	for i := 0; i < typ.NumMethod(); i++ {
		m := typ.Method(i)
		mtype := m.Type
		mname := m.Name
		// Method must be exported.
		if m.PkgPath != "" {
			continue
		}
		// Method needs four ins: receiver, context.Context, *args, *reply.
		if mtype.NumIn() != 4 {
			if reportErr {
				log.Debug("method ", mname, " has wrong number of ins:", mtype.NumIn())
			}
			continue
		}
		// First arg must be context.Context
		ctxType := mtype.In(1)
		if !ctxType.Implements(typeOfContext) {
			if reportErr {
				log.Debug("method ", mname, " must use context.Context as the first parameter")
			}
			continue
		}

		// Second arg need not be a pointer.
		argType := mtype.In(2)
		if !isExportedOrBuiltinType(argType) {
			if reportErr {
				log.Info(mname, " parameter type not exported: ", argType)
			}
			continue
		}

		// Third arg must be a pointer.
		replyType := mtype.In(3)
		if replyType.Kind() != reflect.Ptr {
			if reportErr {
				log.Info("method", mname, " reply type not a pointer:", replyType)
			}
			continue
		}

		// Reply type must be exported.
		if !isExportedOrBuiltinType(replyType) {
			if reportErr {
				log.Info("method", mname, " reply type not exported:", replyType)
			}
			continue
		}

		// Method needs one out.
		if mtype.NumOut() != 1 {
			if reportErr {
				log.Info("method", mname, " has wrong number of outs:", mtype.NumOut())
			}
			continue
		}

		// The return type of the method must be derror.
		if returnType := mtype.Out(0); returnType != typeOfError {
			if reportErr {
				log.Info("method", mname, " returns ", returnType.String(), " not derror")
			}
			continue
		}

		methods[mname] = &method{exec: m, args: argType, repl: reflect.ValueOf(nil)}
	}
	return methods
}
